package sketch;

import javax.swing.*;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.io.*;
import javax.imageio.*;
import java.util.*;
import parser.*;

/**
 * GraphView draws the graph of a function
 * @author Indrojeet Mondal
 * @version 1.0
 * the details algorithm is in the file ALGORITHM
 */
class GraphView extends JPanel implements MouseWheelListener
{
	//the equation parser object
	private GraphParser parser = new GraphParser();
	
	// the plot settings
	private GraphModel model = new GraphModel();;
	
	
	// the origin of the curve
	private int midX, midY;
	
	// the range of f
	private float yMin, yMax;
	
	private float xScale, yScale;
	
	private final float defScale = 40.0f;
	
	private static final int MARGIN = 20;
	private static final int NPOINTS = 1000;
	
	// the list of points to be plotted
	ArrayList<Point2D.Float> points = new ArrayList<Point2D.Float>();
	
	private Font font = new Font("SansSerif", Font.PLAIN, 14);
	
	public GraphView()
	{
		addMouseWheelListener(this);
		
		midX = midY = 0;
		yMin = yMax = 0;
		xScale = yScale = 0;
	}
	
	/**
	 * Update the graph with a new setting
	 * @param settings the new GraphSettings
	 */
	public void  updateView(GraphModel newModel)
	{
		model = newModel;
		
		repaint();
	}
	
	/**
	 * Zoom in the graph; 
	 */
	public void zoomIn()
	{
		
		if (model.curZoom < 20) // restrict to 20X; higher zoom makes invisible in most cases
			++model.curZoom;
		
		repaint();
		
	} 
	
	/**
	 * Zoom out the graph
	 */
	public void zoomOut()
	{
		if (model.curZoom > 1) // can't zoom below 1X!
			--model.curZoom;
		
		repaint();
	}
	
	public void paintComponent(Graphics g)
	{
		super.paintComponent(g);
	
		Graphics2D g2 = (Graphics2D)g;
		
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		
		
		draw(g2);
	}
	
	/*
	 * 
	 */
	private void draw(Graphics2D g2)
	{
		// first, compute the points to be plotted
		computePoints();
		
		Dimension dim = getSize();
		midX = dim.width / 2; // find centre of the widget
		midY = dim.height / 2;
		
		// find scales
		xScale = calculateScale(model.xMin, model.xMax, dim.width-2*MARGIN);
		yScale = calculateScale(yMin, yMax, dim.height-2*MARGIN);
		xScale *= model.curZoom;
		yScale *= model.curZoom;
		
		// fill the bkgnd
		g2.setPaint(model.backColor);
		g2.fillRect(0, 0, dim.width, dim.height);
		
		// shift the origin to the centre of widget
		g2.translate(midX, midY);
		
		// draw axes
		g2.setPaint(model.axesColor);
		
		g2.drawLine(-midX+MARGIN, 0, midX-MARGIN, 0); // horizontal axis
		g2.drawLine(0, -midY+MARGIN, 0, midY-MARGIN); // vertical axis
		
		g2.setFont(font);
		
		// show start and end points on both the axes
		markPoint(g2, model.xMin, true);
		markPoint(g2, model.xMax, true);
		markPoint(g2, yMin, false);
		markPoint(g2, yMax, false);
		
		g2.setPaint(model.curveColor);
		
		// draw equation 
		g2.drawString(model.getEqnString(), -midX+15, -midY+20);
		
		// restrict drawing to margins
		g2.setClip(-midX+MARGIN-1, -midY+MARGIN-1, 2*(midX-MARGIN)+1, 2*(midY-MARGIN)+1);
		
		Line2D.Float curveJoin = new Line2D.Float();
		
		// now plot the points
		int lindex = points.size() - 1; // 
		for (int i = 0; i < lindex; i++) {	
			Point2D.Float pt1 = points.get(i);
			//pt1.setLocation(xScale*pt1.x, -yScale*pt1.y);
			
			Point2D.Float pt2 = points.get(i+1);
			//pt2.setLocation(xScale*pt2.x, -yScale*pt2.y);
			
			curveJoin.setLine(xScale*pt1.x, -yScale*pt1.y, xScale*pt2.x, -yScale*pt2.y);
			g2.draw(curveJoin);
		}
		
	}
	
	/**
	 * draw a tick mark and coordinate of a point
	 * @param pt the point to draw
	 * @param xaxis is the point is on x-axis
	 */
	private void markPoint(Graphics2D g2, float pt, boolean xaxis)
	{
		if (Math.round(pt) == 0.0) // ignore origin
			return;
		
		Line2D.Float tick = new Line2D.Float();
		float ptLoc = 0;
		
		if (xaxis) {
			ptLoc = pt * xScale;
			tick.setLine(ptLoc, -5, ptLoc, +5);
			g2.draw(tick);
			g2.drawString(String.format("%.2f", pt), (float)ptLoc-15, (float)20);
		}
		else {
			ptLoc = pt * -yScale;
			tick.setLine(-5, ptLoc, +5, ptLoc);
			g2.draw(tick);
			g2.drawString(String.format("%.2f", pt), (float)15, (float)ptLoc);
		}
	}
	private void computePoints()
	{	
		parser.reset(); // clear the parser and points stack
		points.clear();
		
		if (model.xMin == model.xMax) // not an interval!
			return;
		
		float span = model.xMax - model.xMin;
		
		
		for (int i = 0; i < NPOINTS; i++) {
			// take a number 0 <= h <= 1 so that a <= a + (b - a) * h <= b
			float x = model.xMin + span * ((float)i / NPOINTS);
			
			parser.setValue((model.eqnType == GraphModel.CARTESIAN) ? "x" : "t", x);
			
			float xt = 0, y = 0;
			
			try {
				if (model.eqnType == GraphModel.PARAMETRIC) {
					xt = (float)parser.parse(model.equation1+"\n");
					y = (float)parser.parse(model.equation2+"\n");
					
					// in case of parametric, x must be admissible too
					if (Float.isNaN(xt) || Float.isInfinite(xt))
						continue;
				}
				else
					y = (float)parser.parse(model.equation1+"\n");
				
				// y must be admissible
				if (Float.isNaN(y) || Float.isInfinite(y))
					continue;
			} catch (ParseException e) {
				e.printStackTrace();
				return;
			}
			
			// add the points to stack
			if (model.eqnType == GraphModel.PARAMETRIC)
				points.add(new Point2D.Float(xt, y));
			else if (model.eqnType == GraphModel.POLAR) 
				// polar coords must be changed to cartesian by r*cos(t),r*sin(t)
				points.add(new Point2D.Float(y*(float)Math.cos(x), y*(float)Math.sin(x)));
			else
				points.add(new Point2D.Float(x, y));
		}
		
		yMin = points.get(0).y;
		yMax = points.get(0).y;
		
		// find the min and max values of y i.e, range of f; the vertical scale(yScale) depends on it 
		for (Point2D.Float pt : points) {
			float y = pt.y;
			if (y < yMin) yMin = y;
			if (y > yMax) yMax = y;
		}
	}
	
	private float calculateScale(float start, float end, int axisLength)
	{
		
		float span = end - start;
		float startDist = Math.abs(0-start), endDist = Math.abs(0-end); // distances of the points start, end from origin
		
		if (span < 3.0) { // too small!
			return defScale;
		}
		if (startDist == endDist) // the interval is symmetric; i.e, of the form [-a, +a]
			return axisLength / span;
		else if ((start < 0 && end == 0) || (start == 0 && end > 0)) // form [a, 0] or [0, b]; use one side of axis
			return (axisLength / 2.0f) / span;
		else if (start < 0 && end > 0) { // starts from left & ends in right but not symmetric; use the max. dist. of side 
			return (axisLength / 2.0f) / Math.max(startDist, endDist);
		}
		else {
			//usedDefScale = true;
			return defScale;
		}
	}
	
	/**
	 * Save the graph in a PNG file
	 * @param file the File to be saved
	 * @throws IOException
	 */
	public void export(File file) throws IOException 
	{
		BufferedImage img = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
		Graphics2D g2 = (Graphics2D)(img.getGraphics());
		draw(g2);
		ImageIO.write(img, "png", file);
	}
	
	/**
	 * Handle mouse wheel event
	 */
	public void mouseWheelMoved(MouseWheelEvent e) 
	{
		int type = e.getWheelRotation();
		if (type > 0)
			zoomIn();
		else
			zoomOut();
	}
	
}
